﻿//LocalMarketDataDrive.cs
//Copyright (c) 2013 StockSharp LLC, all rights reserved.
//This code module is part of StockSharp library.
//This code is licensed under the GNU GENERAL PUBLIC LICENSE Version 3.
//See the file License.txt for the license details.
//More info on: http://stocksharp.com

namespace StockSharp.Algo.Storages
{
	using System;
	using System.Collections.Generic;
	using System.IO;
	using System.Linq;

	using IOPath = System.IO.Path;

	using Ecng.Collections;
	using Ecng.Common;
	using Ecng.Interop;
	using Ecng.Serialization;
	using Ecng.ComponentModel;

	using StockSharp.Algo.Candles;
	using StockSharp.BusinessEntities;

	/// <summary>
	/// Файловое хранилище маркет-данных.
	/// </summary>
	public class LocalMarketDataDrive : BaseMarketDataDrive
	{
		private sealed class LocalMarketDataStorageDrive : IMarketDataStorageDrive
		{
			private readonly LocalMarketDataDrive _parent;
			private readonly Security _security;
			private readonly string _fileName;
			private readonly bool _alphabeticPath;

			private const string _dateFileSuffix = "dates.bin";
			private static readonly Version _dateVersion = new Version(1, 0);
			private const string _dateFormat = "yyyy_MM_dd";

			public LocalMarketDataStorageDrive(LocalMarketDataDrive parent, Security security, Type dataType, string fileName, bool alphabeticPath)
			{
				if (parent == null)
					throw new ArgumentNullException("parent");

				if (security == null)
					throw new ArgumentNullException("security");

				if (dataType == null)
					throw new ArgumentNullException("dataType");

				if (fileName.IsEmpty())
					throw new ArgumentNullException("fileName");

				_parent = parent;
				_security = security;
				_fileName = fileName;
				_alphabeticPath = alphabeticPath;

				_dates = new Lazy<CachedSynchronizedOrderedDictionary<DateTime, DateTime>>(() =>
				{
					var retVal = new CachedSynchronizedOrderedDictionary<DateTime, DateTime>();

					var datesPath = GetDatesCachePath();

					if (File.Exists(datesPath))
					{
						foreach (var date in LoadDates())
							retVal.Add(date, date);
					}
					else
					{
						var rootDir = Path;

						if (Directory.Exists(rootDir))
						{
							var dates = Directory.GetDirectories(rootDir)
								.Where(dir => File.Exists(IOPath.Combine(dir, _fileName + ".bin")))
								.Select(dir => IOPath.GetFileName(dir).ToDateTime(_dateFormat));

							foreach (var date in dates)
								retVal.Add(date, date);
						}

						SaveDates(retVal.CachedKeys);
					}

					return retVal;
				}).Track();
			}

			private string Path
			{
				get
				{
					if (_alphabeticPath)
						return IOPath.Combine(_parent.Path, _security.Id.Substring(0, 1), _security.Id);
					
					return IOPath.Combine(_parent.Path, _security.Id);
				}
			}

			IEnumerable<DateTime> IMarketDataStorageDrive.Dates
			{
				get { return Dates.CachedKeys; }
			}

			private readonly Lazy<CachedSynchronizedOrderedDictionary<DateTime, DateTime>> _dates;

			private CachedSynchronizedOrderedDictionary<DateTime, DateTime> Dates
			{
				get { return _dates.Value; }
			}

			public void ClearDatesCache()
			{
				if (Directory.Exists(Path))
					File.Delete(GetDatesCachePath());

				ResetCache();
			}

			void IMarketDataStorageDrive.Delete(DateTime date)
			{
				var path = GetPath(date);

				if (File.Exists(path))
				{
					File.Delete(path);

					var dir = GetDirectoryName(path);

					if (Directory.EnumerateFiles(dir).IsEmpty())
					{
						IOHelper.BlockDeleteDir(dir);
					}
				}

				var dates = Dates;

				dates.Remove(date);
				SaveDates(dates.CachedKeys);
			}

			void IMarketDataStorageDrive.Save(DateTime date, byte[] rawData)
			{
				Save(date, path => File.WriteAllBytes(path, rawData));
			}

			byte[] IMarketDataStorageDrive.Load(DateTime date)
			{
				return Load(date, File.ReadAllBytes, null);
			}

			void IMarketDataStorageDrive.SaveStream(DateTime date, Stream rawData)
			{
				Save(date, path =>
				{
					using (var file = File.OpenWrite(path))
						rawData.CopyTo(file);
				});
			}

			Stream IMarketDataStorageDrive.LoadStream(DateTime date)
			{
				return Load(date, File.OpenRead, Stream.Null);
			}

			private void Save(DateTime date, Action<string> action)
			{
				Directory.CreateDirectory(GetDataPath(date));

				action(GetPath(date));

				var dates = Dates;

				dates[date] = date;
				SaveDates(dates.CachedKeys);
			}

			private T Load<T>(DateTime date, Func<string, T> action, T defaultValue)
			{
				var path = GetPath(date);
				return File.Exists(path) ? action(path) : defaultValue;
			}

			private string GetDatesCacheFileName()
			{
				return _fileName + _dateFileSuffix;
			}

			private string GetDatesCachePath()
			{
				return IOPath.Combine(Path, GetDatesCacheFileName());
			}

			private IEnumerable<DateTime> LoadDates()
			{
				using (var file = new FileStream(GetDatesCachePath(), FileMode.Open, FileAccess.Read))
				{
					var version = new Version(file.ReadByte(), file.ReadByte());

					if (version > _dateVersion)
						throw new InvalidOperationException("Файл {0} имеет более новый формат версии {1}, чем текущий {2}.".Put(GetDatesCacheFileName(), version, _dateFormat));

					var count = file.Read<int>();

					var dates = new DateTime[count];

					for (var i = 0; i < count; i++)
					{
						dates[i] = file.Read<DateTime>();
					}

					return dates;
				}
			}

			private void SaveDates(DateTime[] dates)
			{
				try
				{
					if (!Directory.Exists(Path))
					{
						if (dates.IsEmpty())
							return;

						Directory.CreateDirectory(Path);
					}
					
					var path = GetDatesCachePath();

					using (var file = new FileStream(path, FileMode.Create, FileAccess.Write))
					{
						file.WriteByte((byte)_dateVersion.Major);
						file.WriteByte((byte)_dateVersion.Minor);
						file.Write(dates.Length);

						foreach (var date in dates)
						{
							file.Write(date);
						}
					}
				}
				catch (UnauthorizedAccessException)
				{
					// если папка с данными с правами только на чтение
				}
			}

			private string GetDataPath(DateTime date)
			{
				return IOPath.Combine(Path, date.ToString(_dateFormat));
			}

			private string GetPath(DateTime date)
			{
				var result = IOPath.Combine(GetDataPath(date), _fileName + ".bin");
				System.Diagnostics.Trace.WriteLine("FileAccess: "+result);
				return result;
			}

			private static string GetDirectoryName(string path)
			{
				var name = IOPath.GetDirectoryName(path);

				if (name == null)
					throw new ArgumentException("Для директории {0} не было найдено названия.".Put(path));

				return name;
			}

			public void ResetCache()
			{
				_dates.Reset();
			}
		}

		private static readonly SynchronizedDictionary<Type, string> _fileNames = new SynchronizedDictionary<Type, string>
		{
			{ typeof(Trade), "trades" },
			{ typeof(MarketDepth), "quotes" },
			{ typeof(OrderLogItem), "orderLog" },
			{ typeof(OrderTraceItem), "orderTrace" },
			{ typeof(SecurityChange), "security" },
		};

		private readonly SynchronizedDictionary<Tuple<Security, Type, object>, LocalMarketDataStorageDrive> _drives = new SynchronizedDictionary<Tuple<Security, Type, object>, LocalMarketDataStorageDrive>();

		/// <summary>
		/// Создать <see cref="LocalMarketDataDrive"/>.
		/// </summary>
		public LocalMarketDataDrive()
			: this(Directory.GetCurrentDirectory())
		{
		}

		/// <summary>
		/// Создать <see cref="LocalMarketDataDrive"/>.
		/// </summary>
		/// <param name="path">Путь к директории с данными.</param>
		/// <param name="useAlphabeticPath">Использовать ли алфавитный путь к данным.</param>
		public LocalMarketDataDrive(string path, bool useAlphabeticPath = false)
		{
			Path = path;
			UseAlphabeticPath = useAlphabeticPath;
		}

		/// <summary>
		/// Создать <see cref="LocalMarketDataDrive"/>.
		/// </summary>
		public LocalMarketDataDrive(bool useAlphabeticPath)
			: this(Directory.GetCurrentDirectory())
		{
			UseAlphabeticPath = useAlphabeticPath;
		}

		private string _path;

		/// <summary>
		/// Путь к директории с данными.
		/// </summary>
		public string Path
		{
			get { return _path; }
			set
			{
				if (value.IsEmpty())
					throw new ArgumentNullException("value");

				if (Path == value)
					return;

				lock (_drives.SyncRoot)
				{
					_path = value;
					_drives.Values.ForEach(d => d.ResetCache());
				}
			}
		}

		/// <summary>
		/// Использовать ли алфавитный путь к данным.
		/// </summary>
		public bool UseAlphabeticPath { get; set; }

		/// <summary>
		/// Получить для инструмента доступные типы свечек с параметрами.
		/// </summary>
		/// <param name="security">Инструмент.</param>
		/// <returns>Доступные типы свечек с параметрами.</returns>
		public override IEnumerable<Tuple<Type, object[]>> GetCandleTypes(Security security)
		{
			if (security == null)
				throw new ArgumentNullException("security");

			return Directory
				.GetDirectories(IOPath.Combine(Path, security.Id))
				.SelectMany(dir => Directory.GetFiles(dir, "candles_*").Select(IOPath.GetFileNameWithoutExtension))
				.Distinct()
				.Select(fileName =>
				{
					var parts = fileName.Split('_');
					var type = "{0}.{1}, {2}".Put(typeof(TimeFrameCandle).Namespace, parts[1], typeof(TimeFrameCandle).Assembly.FullName).To<Type>();
					object value;

					if (type == typeof(TimeFrameCandle))
					{
						value = parts[2].Replace("-", ":").To<TimeSpan>();
					}
					else if (type == typeof(TickCandle))
					{
						value = parts[2].To<int>();
					}
					else if (type == typeof(VolumeCandle))
					{
						value = parts[2].To<decimal>();
					}
					else if (type == typeof(RangeCandle) || type == typeof(RenkoCandle))
					{
						value = parts[2].To<Unit>();
					}
					else if (type == typeof(PnFCandle))
					{
						value = parts[2].To<PnFArg>();
					}
					else
						throw new NotSupportedException("Тип свечек {0} не поддерживается.".Put(type));

					return new Tuple<Type, object>(type, value);
				})
				.GroupBy(t => t.Item1)
				.Select(g => new Tuple<Type, object[]>(g.Key, g.Select(t => t.Item2).ToArray()))
				.ToArray();
		}

		/// <summary>
		/// Создать хранилище для <see cref="IMarketDataStorage"/>.
		/// </summary>
		/// <param name="security">Инструмент.</param>
		/// <param name="dataType">Тип маркет-данных.</param>
		/// <param name="arg">Параметр, ассоциированный с типом <paramref name="dataType"/>. Например, <see cref="Candle.Arg"/>.</param>
		/// <returns>Хранилище для <see cref="IMarketDataStorage"/>.</returns>
		public override IMarketDataStorageDrive GetStorageDrive(Security security, Type dataType, object arg)
		{
			return _drives.SafeAdd(new Tuple<Security, Type, object>(security, dataType, arg), key =>
			{
				if (!_fileNames.ContainsKey(dataType) && arg == null)
					throw new ArgumentNullException("arg");

				var fileName = _fileNames.TryGetValue(dataType) ?? "candles_{0}_{1}".Put(dataType.Name, arg.ToString().Replace(":", "-"));

				return new LocalMarketDataStorageDrive(this, security, dataType, fileName, UseAlphabeticPath);
			});
		}
	}
}